# Copyright 2020 Broadband Forum
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""A MIB (Management Information Base), also referred to as an ME (Managed
Entity), is defined via a `MIB` instance.

A MIB definition describes its `Attrs <Attr>` (attributes), `Actions <Action>`,
`Notifications <Notification>`, `Changes <Change>` and `Alarms <Alarm>`.

Example::

    onu_data_mib = MIB(2, 'ONU_DATA', 'Models the MIB itself', attrs=(
      Attr(0, 'me_inst', 'Managed entity instance', R, M, Number(2, fixed=0)),
      Attr(1, 'mib_data_sync', 'MIB data sync', RW, M, Number(1))
    ), actions= (
      get_action, set_action, get_all_alarms_action,
      get_all_alarms_next_action, mib_reset_action, mib_upload_action,
      mib_upload_next_action
    ))

"""

import logging

from typing import Dict, Optional, Tuple, Union

from .action import Action
from .types import AttrData, AttrDataValues, Datum, Name, NumberName, \
    AutoGetter

logger = logging.getLogger(__name__.replace('obbaa_', ''))

mibs: Dict[int, 'MIB'] = {}
"""All instantiated MIBs."""


class MIB(NumberName, AutoGetter):
    """MIB definition class.
    """

    def __init__(self, number: int, name: str, description: str = None, *,
                 attrs: Tuple['Attr', ...] = None,
                 actions: Tuple[Action, ...] = None,
                 notifications: Tuple['Notification', ...] = None,
                 changes: Tuple['Change', ...] = None,
                 alarms: Tuple['Alarm', ...] = None):
        """MIB class constructor.

        The constructor adds new MIB instances to the `mibs` dictionary.

        Args:
            number: MIB number; MUST be unique across all MIB
                instances; 0-65535.

            name: MIB name; MUST be unique across all MIB instances.

            description: MIB description; used only for documentation purposes.

            attrs: MIB attributes. They're defined as a tuple but each has a
                number, so order doesn't matter.

            actions: Actions valid for use with this MIB.

            notifications: Notifications generated by this MIB.

            changes: Changes generated by this MIB.

            alarms: Alarms generated by this MIB.
        """
        assert number not in mibs
        super().__init__(number, name, description)
        self._attrs = attrs and tuple(attrs) or ()
        self._actions = actions and tuple(actions) or ()
        self._notifications = notifications and tuple(notifications) or ()
        self._changes = changes and tuple(changes) or ()
        self._alarms = alarms and tuple(alarms) or ()

        # XXX should check that changes reference defined attributes

        mibs[number] = self

    # XXX should allow abbreviation (and case independence?)
    def attr(self, number_or_name: Union[int, str]) -> Optional['Attr']:
        """Find a MIB attribute by number or name, returning the attribute or
        None.

        Args:
            number_or_name: MIB attribute number or name.

        Returns:
            `Attr` instance or ``None`` if not found.
        """
        prop_name = isinstance(number_or_name, int) and 'number' or 'name'
        attrs = [a for a in self._attrs if getattr(a, prop_name) ==
                 number_or_name]
        assert len(attrs) <= 1
        return attrs[0] if len(attrs) == 1 else None

    def attr_names(self, access: 'Access' = None) -> str:
        """Return a (string representation of) a list of all attribute
        names, optionally restricted to those with a specified access level.

        Args:
            access: Desired access level, or ``None`` to return all attributes.

        Returns:
            String of the form ``name1, name2, name3``.

        Note:
            Attribute 0 is ``me_inst`` (the ME instance number) and is not
            returned.
        """
        return ', '.join(
                str(a) for a in sorted(self._attrs, key=lambda a: a.number) if
                a.number > 0 and access in {None, a.access})


class Attr(NumberName, AutoGetter):
    """MIB attribute class.
    """

    def __init__(self, number: int, name: str, description: str = None,
                 access: 'Access' = None, requirement: 'Requirement' = None,
                 data: AttrData = None):
        """MIB attribute constructor.

        Args:
            number: attribute number; MUST be unique within its MIB; 0-16.

            name: attribute name; MUST be unique within its MIB.

            description: attribute description; used only for documentation
                purposes.

            access: attribute access level.

            requirement: attribute support requirement

            data: attribute data specification.
        """
        assert 0 <= number <= 16
        super().__init__(number, name, description)

        self._access = access
        self._requirement = requirement

        assert isinstance(data, Datum) or (
                isinstance(data, tuple) and all(isinstance(i, Datum) for
                                                i in data))
        self._data = isinstance(data, Datum) and (data,) or data

    def decode(self, content: bytearray, offset: int) -> \
            Tuple[AttrDataValues, int]:
        """Decode a MIB attribute value.

        Args:
            content: buffer from which to decode the value.

            offset: byte offset within buffer at which to start decoding.

        Returns:
            Decoded values and updated offset.

            * The decoded values are returned as a tuple (if there's more than
              one value), a single value, or ``None``
            * The offset is ready to be passed to the next ``decode``
              invocation
        """
        values = []
        for datum in self._data:
            value, offset = datum.decode(content, offset)
            values += [value]
        values = tuple(values) if len(values) > 1 else values[0] if len(
                values) == 1 else None
        return values, offset

    def encode(self, values: AttrDataValues = None) -> bytearray:
        """Encode a MIB attribute value.

        Args:
            values: tuple, single value or ``None``.

        Returns:
            Encoded buffer. If ``None`` was supplied, an empty buffer is
            returned.
        """
        buffer = bytearray()
        if values:
            values_ = values if isinstance(values, tuple) else (values,)
            assert len(values_) == len(self._data)
            for i, datum in enumerate(self._data):
                buffer += datum.encode(values_[i])
        return buffer

    # XXX what if need arguments (use closure)? what if return type is wrong
    #     (check)?
    def resolve(self, values: AttrDataValues = None) -> AttrDataValues:
        """Resolve a MIB attribute value by invoking any callable values.

        Any callable values are invoked (with no arguments) and are replaced
        with the returned value. See `Database` for an example.

        Args:
            values: tuple, single value or ``None``.

        Returns:
            Resolved values. If ``None`` was supplied, then ``None`` is
            returned.
        """
        results = None
        if values:
            values_ = values if isinstance(values, tuple) else (values,)
            assert len(values_) == len(self._data)
            results_ = tuple(v() if callable(v) else v for v in values_)
            results = results_ if isinstance(values, tuple) else results_[0]
        return results

    @property
    def mask(self):
        """Get this attribute's mask, e.g. the value to use when forming the
        `Get` command's ``attr_mask`` field.

        Note:
            Don't call this on attribute 0 (``me_inst``). You'll get an
            assertion failure.
        """
        # 'me_id' is attribute 0 but it makes no sense to get its mask
        assert 1 <= self._number <= 16
        shift = 16 - self._number  # 15, ..., 0
        return 1 << shift

    @property
    def size(self):
        """Get this attribute's value size in bytes."""
        return sum(d.size for d in self._data)


class Access(Name, AutoGetter):
    """Attribute access class (G.988 Section 9).
    """
    def __init__(self, name):
        super().__init__(name, names={'R', 'W', 'RW', 'RC', 'RWC'})

R = Access('R')         #: Read-only access.
W = Access('W')         #: Write-only access.
RW = Access('RW')       #: Read-Write access.
RC = Access('RC')       #: Read and set-by-Create access.
RWC = Access('RWC')     #: Read-Write and set-by-Create access.


class Requirement(Name, AutoGetter):
    """Attribute requirement class.
    """

    def __init__(self, name):
        super().__init__(name, names={'M', 'O'})

M = Requirement('M')    #: Mandatory
# noinspection PyPep8
O = Requirement('O')    #: Optional


class Notification(Name, AutoGetter):
    """Attribute notification class.
    """
    pass

test_result_notification = Notification('test_result')
"""Test result notification."""


# XXX changes are AVC (Attribute Value Change) notifications
# XXX they would be better indicated via an Attr avc property
class Change(NumberName):
    """Attribute change class.
    """
    pass


class Alarm(NumberName):
    """Attribute alarm class.
    """
    pass
